<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>NML Player</title>
    <script src="nmlc.js"></script>
  </head>
  <body>
    <textarea id="source"></textarea>
    <div>
      <button onclick="start()">Start</button>
      <button onclick="stop()">Stop</button>
      <input id="speed">
    </div>
    <script>
      function isNumber(n) {
        return typeof(n) == 'number' && isFinite(n)
      }

      function sleep(ms) {
        return new Promise(r => setTimeout(() => r(), ms))
      }

      const ctx = new AudioContext()

      const sounds = {}

      const notes = {
        wave(type) {
          if (['sine', 'square', 'sawtooth', 'triangle'].indexOf(type) < 0) {
            throw `Type '${type}' not supported`
          }
          return (pitch, duration, dest) => {
            const source = ctx.createOscillator()
            source.type = type
            source.frequency.value = 440 * Math.pow(2, pitch / 12)
            source.connect(dest)
            source.start()
            source.stop(ctx.currentTime + duration / 1000)
          }
        },
        customWave(type) {
          if (!type || !Array.isArray(type[0]) || !Array.isArray(type[1])) {
            throw 'Type should be [Array, Array]'
          }
          const wave = ctx.createPeriodicWave(new Float32Array(type[0]), new Float32Array(type[1]))
          return (pitch, duration, dest) => {
            const source = ctx.createOscillator()
            source.setPeriodicWave(wave)
            source.frequency.value = 440 * Math.pow(2, pitch / 12)
            source.connect(dest)
            source.start()
            source.stop(ctx.currentTime + duration / 1000)
          }
        },
        sound(type) {
          if (!sounds[type]) {
            throw `Type '${type}' not supported`
          }
          return (pitch, duration, dest) => {
            const source = ctx.createBufferSource()
            source.buffer = sounds[type]
            source.playbackRate.value = Math.pow(2, pitch / 12)
            source.connect(dest)
            source.start()
            // source.stop(ctx.currentTime + duration / 1000)
          }
        }
      }

      let playing = null

      let speed = 1

      async function play(music) {
        if (!Array.isArray(music.tracks) || !music.tracks.length) {
          throw 'Tracks required'
        }
        if (!Array.isArray(music.notes) || !music.notes.length) {
          throw 'Notes required'
        }
        const gains = []
        const basePitches = []
        const noteFormats = []
        for (const track of music.tracks) {
          const gain = ctx.createGain()
          gain.gain.value = isNumber(track.volume) && track.volume > 0 ? track.volume : 1
          gain.connect(ctx.destination)
          gains.push(gain)
          basePitches.push(isNumber(track.basePitch) ? track.basePitch : 1)
          noteFormats.push(notes[track.noteFormat](track.noteType))
        }
        if (playing) {
          stop()
        }
        const currentPlaying = {}
        playing = currentPlaying
        let lastTime = 0
        for (const [track, time, pitch, duration] of music.notes) {
          if (!isNumber(time) ||
              !music.tracks[track] ||
              !isNumber(pitch) ||
              !isNumber(duration) || duration <= 0) {
            continue
          }
          const span = time - lastTime
          if (span > 0) {
            await sleep(span / speed)
            if (playing != currentPlaying) {
              break
            }
          }
          noteFormats[track](basePitches[track] + pitch, duration / speed, gains[track])
          lastTime = time
        }
      }

      function stop() {
        playing = null
      }

      function start() {
        const speedStr = document.getElementById('speed').value
        speed = speedStr == '' ? 1 : speedStr
        if (speed < 0.1 || speed > 10) {
          alert('Speed should be between 0.1 and 10')
          return
        }
        const source = document.getElementById('source').value
        let result
        try {
          result = nmlc(source)
        } catch (e) {
          alert(e)
          return
        }
        play(result).catch(e => alert(e))
      }

      function test() {
        play({
          tracks: [
            {
              noteFormat: 'customWave',
              noteType: [
                [0, 1, 0.4, 0.6, 0.2],
                [0, 0, 0, 0, 0]
              ]
            }
          ],
          notes: [
            [0, 0, 0, 1000]
          ]
        })
      }
    </script>
  </body>
</html>
